← All docs

Eval Runtime Architecture

How literal eval is planned for AOT lowering, when Magician is linked, and how native and interpreted scopes share values.

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. 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.

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

PathTypical inputRuntime requirements
Direct AOTA literal fragment whose operations and caller-local reads/writes can be lowered staticallyNo eval context, scope, or Magician library
Scope-backed AOTA literal fragment that is statically lowerable but needs materialized known scope valuesCore eval_scope runtime feature; no interpreter library
Interpreter fallbackA dynamic string or a literal requiring dynamic declarations, includes, references, dynamic calls, or another unsupported AOT shapeeval_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:

OperationResponsibility
EvalLiteralCallPreserve a literal fragment for AOT planning or interpreter fallback.
EvalScopeGet, EvalScopeSetRead or update a named boxed cell in a materialized eval scope.
EvalFunctionCall, EvalFunctionCallArrayDispatch a function created or registered in the persistent eval context.
EvalFunctionExists, EvalClassExistsProbe dynamic symbols that may have been created by an earlier eval barrier.
EvalConstantExists, EvalConstantFetchProbe or fetch constants retained in the eval context.
EvalObjectNewConstruct a class that may have been declared at runtime.
EvalStaticMethodCallDispatch 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:

LocalKindLifetime and role
EvalContextPersistent Magician context for declarations, constants, callable metadata, and interpreter state. Its presence requires the full bridge.
EvalScopeMaterialized activation/closure scope shared with the executing fragment.
EvalGlobalScopeMaterialized 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<EvalProgram> 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. Per-builtin AOT/eval availability is generated from the builtin! and eval_builtin! registries in the Builtin Reference. The Runtime documents the shared __rt_* assembly families and links here for the optional eval boundary.