diff --git a/.claude/skills/update-builtin-docs/SKILL.md b/.claude/skills/update-builtin-docs/SKILL.md
new file mode 100644
index 0000000000..d0bfe74cae
--- /dev/null
+++ b/.claude/skills/update-builtin-docs/SKILL.md
@@ -0,0 +1,51 @@
+---
+name: update-builtin-docs
+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
+
+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 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 --example 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/` (`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:
+
+```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/.config/nextest.toml b/.config/nextest.toml
index 640e47603b..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"
@@ -46,3 +49,48 @@ 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 }
+
+# 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 8c0426a726..896a8f2abf 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
@@ -233,7 +237,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 +266,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 +306,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
@@ -363,11 +367,14 @@ 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" \
--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
@@ -408,11 +415,14 @@ 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" \
--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
@@ -453,15 +463,154 @@ 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" \
--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
@@ -505,10 +654,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
@@ -584,8 +734,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 +750,5 @@ jobs:
path: |
benchmark-results.json
benchmark-results.md
+ magician-benchmark-results.json
+ magician-benchmark-results.md
diff --git a/.plans/elephc-eval-magician-plan.md b/.plans/elephc-eval-magician-plan.md
new file mode 100644
index 0000000000..6463865856
--- /dev/null
+++ b/.plans/elephc-eval-magician-plan.md
@@ -0,0 +1,800 @@
+# Plan: eval, elephc-magician, and Literal Eval AOT
+
+## Task
+
+- [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] 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] 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.
+- [ ] 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 `$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,
+ 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
+ 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`
+
+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.
+
+## Current State
+
+Eval support has two paths:
+
+1. Runtime fallback through `libelephc-magician`, called by
+ `__elephc_eval_execute`.
+2. Literal eval AOT, when the fragment is a compile-time-known string and the
+ classifier considers it semantically safe.
+
+These paths intentionally use different frontends today:
+
+- literal AOT reuses the **compiler** lexer/parser (`src/lexer/`, `src/parser/`)
+ and then classifies the resulting AST;
+- runtime fallback uses the **magician** lexer/parser
+ (`crates/elephc-magician/src/lexer/`, `crates/elephc-magician/src/parser/`)
+ and lowers into EvalIR for the interpreter.
+
+They must not silently disagree on pure PHP fragment validity. Magician remains
+a separate staticlib and must not depend on the full `elephc` crate or ship the
+compiler into user binaries.
+
+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.
+
+Current central files:
+
+- `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`
+
+## Consolidated Architecture
+
+### Magician Fallback
+
+`elephc-magician` is an optional bridge staticlib. Programs without runtime eval
+must not link it. The fallback remains mandatory for:
+
+- 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.
+
+The fallback receives:
+
+- global eval context;
+- local eval scope;
+- global scope when needed;
+- code pointer/length;
+- result buffer.
+
+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
+
+Native code must synchronize with eval scope only for values observable by the
+fragment:
+
+- 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.
+
+When analysis is imprecise, semantics wins over performance: use the fallback or
+treat the fragment as a stronger barrier.
+
+### Literal Eval AOT
+
+The compiler analyzes literal fragments at compile time:
+
+```text
+literal string
+ -> parse as PHP fragment (compiler frontend)
+ -> normalize/name-resolve compatibly with the context
+ -> classify AOT eligibility
+ -> plan reads/writes/calls/fallback
+ -> native lowering or magician fallback
+```
+
+The AOT plan must preserve:
+
+- `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.
+
+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
+
+Completed:
+
+- `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.
+
+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.
+
+### Magician Interpreter
+
+Completed for the current subset:
+
+- runtime lexer/parser for eval fragments without `$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 after the dynamic-name work:
+
+1. arrays/iterables in AOT once COW and ownership are clear;
+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.
+
+### 4. Compiler/Eval Builtin Parity
+
+`tests/builtin_parity_tests.rs` distinguishes:
+
+- shared compiler/eval builtins;
+- documented eval-only builtins;
+- static-only builtins registered in the compiler but not yet exposed by
+ magician.
+
+When a static-only builtin is implemented in magician:
+
+- remove it from the static-only allowlist;
+- add eval signature metadata;
+- add interpreter dispatch;
+- add named/positional tests when relevant;
+- update benchmarks only if the builtin enters an eval hot path.
+
+### 5. Benchmarks and Measurement
+
+The benchmark suite exists. Remaining work:
+
+- decide which AOT benchmarks should become permanent;
+- always exclude compile/assemble/link time from runtime numbers;
+- keep at least one prime-loop case and one algebra-heavy case as a manual
+ regression or CI artifact;
+- preserve output correctness against PHP where practical.
+
+### 6. Documentation
+
+Update docs when the subset changes:
+
+- eval enables an optional dynamic runtime;
+- 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;
+- 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 `` / `
+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
+cargo check
+cargo test --test codegen_tests
+./scripts/test-linux-x86_64.sh
+./scripts/test-linux-arm64.sh
+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
+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
+```
+
+## Risks
+
+- 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 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.
+- 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.
+
+## 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. 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;
+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).
diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md
new file mode 100644
index 0000000000..020a6fcf93
--- /dev/null
+++ b/.plans/elephc-magician-builtin-structure-plan.md
@@ -0,0 +1,286 @@
+# Plan: elephc-magician Builtin Structure
+
+## Task
+
+- [x] Phase 1: introduce a declarative eval-side registry without changing
+ runtime behavior.
+- [x] Phase 1: migrate a small pilot set of simple builtins into the new
+ 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.
+- [x] Phase 2: migrate already implemented magician builtins area by area while
+ keeping fallback to existing dispatchers until each area is complete.
+- [x] Phase 2: remove duplicate manual tables for names, signatures, defaults,
+ 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.
+- [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.
+- [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.
+- [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.
+- [x] Phase 4: merge the remaining declaration-only builtin home files with
+ their PHP-visible direct/by-value implementations, area by area.
+
+## 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;
+- the PHP-visible builtin implementation for that entry.
+
+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
+
+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 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:
+
+- 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.
+
+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.
+
+## 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.
+
+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,
+ 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
+ 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/AGENTS.md b/AGENTS.md
index 6245315fa5..91921168a1 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 --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
+ 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/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/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/Cargo.lock b/Cargo.lock
index 24407fff5d..f8f865759c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -584,6 +584,7 @@ dependencies = [
"bzip2-rs",
"elephc-crypto",
"elephc-image",
+ "elephc-magician",
"elephc-pdo",
"elephc-phar",
"elephc-tls",
@@ -625,6 +626,18 @@ dependencies = [
"tiny-skia",
]
+[[package]]
+name = "elephc-magician"
+version = "0.1.0"
+dependencies = [
+ "elephc-crypto",
+ "elephc-phar",
+ "flate2",
+ "inventory",
+ "libc",
+ "unicode-segmentation",
+]
+
[[package]]
name = "elephc-pdo"
version = "0.1.0"
@@ -2588,6 +2601,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/Cargo.toml b/Cargo.toml
index 2b3d71d6e7..e2cef692a4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,20 +17,24 @@ 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_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"]
-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-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]]
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"
@@ -60,6 +64,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-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/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/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/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 @@
+ 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/abi.rs b/crates/elephc-magician/src/abi.rs
new file mode 100644
index 0000000000..f53a4eee90
--- /dev/null
+++ b/crates/elephc-magician/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 = 2;
+
+/// 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-magician/src/context.rs b/crates/elephc-magician/src/context.rs
new file mode 100644
index 0000000000..ccc782c6cf
--- /dev/null
+++ b/crates/elephc-magician/src/context.rs
@@ -0,0 +1,61 @@
+//! Purpose:
+//! 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`
+//! - `crate::__elephc_eval_execute()`
+//!
+//! Key details:
+//! - 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;
+#[cfg(not(test))]
+use std::sync::{Mutex, OnceLock};
+
+use crate::abi::ABI_VERSION;
+use crate::eval_ir::{
+ EvalAttribute, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum,
+ EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType,
+ EvalTrait, EvalTraitAdaptation, EvalVisibility,
+};
+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();
+
+thread_local! {
+ static NATIVE_FRAME_CALLED_CLASS_OVERRIDES: RefCell> =
+ RefCell::new(Vec::new());
+}
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