Skip to content

feat: PHP eval() support via the Magician interpreter#451

Merged
nahime0 merged 1208 commits into
mainfrom
feat/eval
Jul 13, 2026
Merged

feat: PHP eval() support via the Magician interpreter#451
nahime0 merged 1208 commits into
mainfrom
feat/eval

Conversation

@nahime0

@nahime0 nahime0 commented Jul 1, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds full eval() support to elephc through Magician (crates/elephc-magician), a PHP interpreter that ships as an optional static library. Programs that never call eval() are completely unaffected: the interpreter is linked only when eval is actually used, and native-only binaries keep their current size and dependencies.

1,207 commits · 2,128 files · +268k/−11.5k lines

How eval is executed

Eval sites are compiled with a three-tier strategy, picked per call site by coordinated classifiers in the plan, lowering, and codegen layers:

  1. Literal AOT — string-literal fragments that the compiler can prove static (scalar expressions, static builtin calls, array literal reads, switch/match/ternary, user-function calls, …) are compiled ahead of time into native EIR functions. No interpreter, no bridge, no runtime cost beyond a direct call.
  2. Scope AOT — fragments that read or write caller locals but are otherwise static run as native functions with direct read parameters or scope write-back, still without linking the interpreter.
  3. Bridge — everything else (dynamic code, declarations, object semantics) executes in the Magician interpreter through the __elephc_eval_* FFI bridge, with bidirectional scope synchronization between native locals and the eval scope.

Native ↔ eval interop

  • Caller locals are synchronized across the eval boundary in both directions, including type-changing writes (scalar slots widen to boxed Mixed storage) and container values.
  • eval'd code can call AOT functions, methods, and constructors with full PHP visibility semantics: private/protected access checks by calling scope, private-method shadowing over child overrides, magic __call/__callStatic fallbacks, and late static binding.
  • eval-declared functions, classes, closures, and constants are callable from subsequent eval fragments and observable through the dynamic probes (function_exists, defined, reflection, …).

The Magician interpreter

  • 415+ PHP builtins implemented through a declarative eval_builtin! registry (inventory-collected, one home file per builtin), covering strings, arrays, math, regex (PCRE + mb_ereg), JSON, filesystem, date/time, SPL, and reflection.
  • Static-vs-eval builtin parity is enforced by dedicated parity gates (tests/builtin_parity_tests.rs).

Documentation

  • New docs/php/eval.md covering supported semantics and the AOT tiers.
  • The generated builtin docs now render a dual support matrix: every builtin page and index shows whether it is available in compiled (AOT) code and inside eval(), with links to both implementations. The exporter reads both registries, so the matrix cannot drift from the parity tests.

Compiler improvements landed along the way

  • PHP 8.3 auto-key semantics for array literals (bool/float/numeric-string and leading-negative keys).
  • array_key_exists() over boxed Mixed containers with runtime tag dispatch; float key normalization.
  • is_array/is_integer dispatchable as runtime string callbacks.
  • ReflectionParameter promoted-parameter metadata, newInstanceArgs() with indexed argument lists, attribute array-literal arguments.
  • First-class-callable fixes (by-ref builtin params, magic fallbacks).

Testing

  • 935 eval codegen tests plus 7 dedicated eval test suites (closures, callables, reflection invocation, …), all green on macOS.
  • 928 Magician unit tests; full codegen/error/parser/lexer suites green.
  • Branch is rebased on latest main (0.26.1 + declare support) with the generated docs byte-identical to CI regeneration.

@nahime0 nahime0 force-pushed the feat/eval branch 5 times, most recently from b1fed8b to 0262885 Compare July 11, 2026 18:44
@nahime0

nahime0 commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

This is almost ready for a first merge. It's still experimental, but it's a good candidate for the next release

@github-actions github-actions Bot added area:magician Touches eval, include execution, or elephc-magician. area:optimizer Touches AST or EIR optimization passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities. labels Jul 13, 2026
nahime0 added 24 commits July 13, 2026 13:32
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.
…rrides

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.
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.
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.
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.
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.
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.
@nahime0 nahime0 changed the title Magician feat: PHP eval() support via the Magician interpreter Jul 13, 2026
@nahime0 nahime0 marked this pull request as ready for review July 13, 2026 13:53
@nahime0 nahime0 merged commit 6146278 into main Jul 13, 2026
2 checks passed
@nahime0 nahime0 deleted the feat/eval branch July 13, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:magician Touches eval, include execution, or elephc-magician. area:optimizer Touches AST or EIR optimization passes. area:types Touches type checking, inference, or compatibility. scope:multi-area Touches more compiler areas than the automatic area-label cap. size:xl Very large pull request that needs deliberate review planning. type:feature Introduces new user-visible behavior or capabilities.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant